home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Shareware Grab Bag
/
Shareware Grab Bag.iso
/
090
/
byte0387.arc
/
EDGINTON.ARC
/
LIST2.TXT
< prev
next >
Wrap
Text File
|
1986-07-16
|
12KB
|
210 lines
This project started as a challenge to make a friend's
calculator program load and remain resident in memory on an IBM
PC. Making a program written in assembly langauge stay resident
has been presented in many articles and books, but writing the
tools to make a C program resident was a new adventure. I
developed all the examples in this article with Lattice's C
Compiler version 3.0 and Microsoft's Macro Assembler 4.0. I have
tried to make everything as portable as possible, but I'm sure
that some modification will have to be made for different
compilers and languages. In the listings, I have noted any
compiler-dependent variables. (Editor's note: William Claff's
article, "xxxxxx" on page ?? contains additional information on
the topic of DOS extension via memory-resident programs.)
WHAT IS A RESIDENT PROGRAM?
DOS uses a set of pointers called Storage Blocks to keep
track of allocated and unallocated memory in the system. For each
loaded program, these pointers indicate the address its PSP
(Program Segment Prefix) the program's length in segments. There
is also a flag that indicates whether or not the memory pointed to
by the Storage Block is allocated. When a program module is
loaded and executes an INT 27H (terminate but stay resident) or
DOS function 31H (keep process), COMMAND.COM makes sure that this
program becomes a part of DOS. This means that the Storage Block,
PSP, and the program module remain in memory and are not
reallocated.
The principles behind making a program resident seems to be
straightforward, just find the length of the program, shove it
into a register and call a documented function. DOS Function 31H
requests the program size in paragraphs be placed in DX and the
return code if any in AL.
As demonstrated by the program shown in listing 1, it is a
simple matter to make a program resident. If you have a utility
like Norton's SI or SMAP you can verify that the program is indeed
resident by looking at the location of the next program to be
loaded address. You can also examine the amount of free memory
displayed by the CHKDSK utility before and after running the
program.
Usually, we want to write a program that is more helpful than
just taking up memory. Specifically, we want to write a program
that responds to a system interrupt, and in doing so, supplies us
with some sort of information. It should also be "well behaved"
and operate within the constraints of DOS.
The design of this installation system had several primary
goals:
* Modular design for universal application.
* Optimum memory usage.
* Correct processing of interrupts.
Modular design means that I can, with minor revision, make
this program load any module that meets with the requirements for
a resident, interrupt processing program. To determine these
requirements, I made a careful analysis of what my compiler did to
a program, and what my linker did to the object modules supplied
to it. If you are using a compiler and/or linker other than the
ones I used, these requirements may be different. Listing 2 is an
example of a completed sample system. Since we have little
control right now over anything that happens above main(), we'll
start there and analyze what happens. Refer to listing 3, which
is a dissasembled version of the top-level code in listing 2.
Cpush() and cpop() are two routines we'll create later to
help us get into and return from the interrupts. Since main() is
really just another function called by the compiler's entry module
which is what is really loaded by EXEC, the BP register is saved
and then set to the new SP. This is a requirement of any
functions called from another routine that might pass any
information on the stack; it allows the functions to reference
that information on the stack via the BP register while still
permitting new data to be pushed on the stack as required.
Entry into a resident program should be designed so any
parameters are passed in the DOS communications area or in
registers, and not on the stack. Also, once a program module is
installed in memory, we want to ignore the call to install().
Although this uses six bytes of memory, passing the address of the
call to cpush() to the interrupt vector is the most efficient way
to install the module. All function names are made common in a C
compiler so we can create the new vector IP by:
nu_entry = (short)main + 6;
Casting main to a short keeps it consistent with the way the
rest of the register structures are typed. nu_entry now points to
the desired entry point in the program. Since we did not need to
use the compiler-generated PUSH BP, and we are returning from an
interrupt we can ignore the POP BP and the RET that the compiler
put at the end of main.
The install() function is straightforward. In this example I
borrowed an unused function call's vector to leave a signature or
message to the calling program that we are already installed. To
increase the safety of this routine, you could verify that the
interrupt vector is filled with zeros first. If it is not, check
another vector until one is found with no vector already
installed. Alternately, you could indicate that the module is
already installed by setting a flag in memory, but you would have
to choose a byte that you are certain would not be used by some
other routine.
Another method for routines that handle passed values (i.e.
video calls, put and get char and string calls) would be to detect
a certain value, and return an 'already installed' message to the
installation program. Listing 4 shows a segment of code that you
could modify to perform this method of signature detection.
The next task is to decide how to best utilize the memory
taken up by the program. Since I used function call 31h instead
of int 21h to terminate the program, loaded programs can exceed
the 64k limit imposed by the latter. I can use .EXE programs with
stack and data segments defined -- not just .COM programs. A .COM
program uses as much memory as the machine has left when it is
loaded; if the program is going to stay resident, it has to return
its unused memory to the system.
I release the memory that contains the program's copy of the
environment using routine d_env(). On entry, the ES and DS (and
SS and CS for a .COM program) segment registers point to the PSP
at offset 0. Listing 5 shows the code for d_env(). I load ES
with the address of the segment containing the copy of the
environment and call DOS function 49H (free allocated memory).
If the program is a .COM file, you can reduce its size using
routine shrink() (see listing 6). This function sets the memory
used by a program to the size of the program module in paragraphs.
If you write .COM programs, be sure that you allocate stack area
before calling this function. If you use shrink(), you should
call it before calling d_env() so that the ES register contains
the correct information for the call to function 4AH (modify
allocated memory blocks). (You could modify the code to perform
both operations with one call to increase the speed and reduce the
size of the program.)
The last area I will cover concerning memory management is
one that is heavily influenced with my familiarity with the
Lattice compiler. This compiler uses a file to set up the segment
registers, handle stack and memory allocations, report errors such
as stack overflows, handle command line arguments to change the
stack size, redirect I/O, and some other incidental operations.
The code for all this is found in the c.asm file -- its object
module is in c.obj. This code is loaded before main() and cannot
be efficiently deallocated by any means other than actually
editing out unused portions of c.asm and recompiling the file. A
knowledgeable programmer should be able to remove large portions
of c.asm for many applications; I have reduced considerable space
in mine.
INTERRUPTS
Now that I've shown how to load programs into memory and keep
them resident, let's examine the available methods of processing
the interrupts (keyboard, clock, etc.) and determine the best
possible way to maintain 'nice' programs. My main concern is with
the saving of registers and flags because of the amount of calls
and subroutines normally found in a program written in C. (You
can see an example of this in listing 1.)
Since we passed the address of cpush() to the interrupt
vector, the first thing the program does when it is entered is a
call to cpush(). This call pushes a return address on the stack,
one that would not be there if the code was being generated in
assembly language. This problem is repeated throughout the
program so it must be handled very early on.
The three modules in listing 7 show one of the fastest and
most efficient solutions I found. Upon entry into the program I
call cpush(). This routine stores the short call return address,
the interrupting program's return CS and IP, and the FLAGS that
are pushed on the stack. It then stores the registers and segment
registers in its own allocated memory. The short return address
is then pushed back onto the stack and the function returns to the
body of the program.
After the interrupt routine does its work (in the example
given in listing 2, it prints "Hello world"), it calls cpop() to
return to the interrupted program. The cpop() routine emulates a
pop of all the registers that should have been pushed onto the
stack upon entry into the interrupt handler, and then does an
IRET. (For debugging purposes, I have also included the code for
cpopt(), which is similar to cpop() except that cpopt() exits via
a RET instruction.)
SUMMARY
This article demonstrates a very simple interrupt processing
program that remains resident in memory. I plan to do more work
in writing programs that process the keyboard and video
interrupts. Any programs written that use these techniques should
be written with proper attention to good program structure, and
correct manipulation of pointers and addresses. This project
turned out to be a lot more ambitious than I originally thought.
The entry and exit routines posed the most problem, testing and
debugging sometimes left the machine in a very corrupted state.
Be certain that your C programs can pass lint before using them;
remember, you are creating a extension of DOS. I did notice that
including structures in a program compiled with Lattice increased
the address of the entry point by three. It makes a call after
main() to set up the memory for the structs and/or unions. I will
be interested in feedback about improving any of these algorithms
and techniques.